package cz.muni.fi.pb138.localnews;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import cz.muni.fi.pb138.localnews.dto.ArticleDTO;

@SuppressWarnings("static-access")
public class XmlStorageHelper implements DataProvider {

	private static final String FILE_SEP = System.getProperty("file.separator");
	private static final String USER_HOME = System.getProperty("user.home")
			+ System.getProperty("file.separator");
	private static final String USER_APP_HOME = USER_HOME + "localNews"
			+ FILE_SEP;

	private static Document doc;

	public static void storeDataToXML(List<ArticleDTO> inputData) {

		loadDataFromStorage();

		Calendar cal = Calendar.getInstance();

		Map<Date, List<ArticleDTO>> sortedArticles = new HashMap<Date, List<ArticleDTO>>();

		for (ArticleDTO article : inputData) {
			if (sortedArticles.containsKey(article.getDate())) {
				sortedArticles.get(article.getDate()).add(article);
			} else {
				sortedArticles.put(article.getDate(),
						new ArrayList<ArticleDTO>());
				sortedArticles.get(article.getDate()).add(article);
			}
		}

		Element rootElem = (Element) doc.getFirstChild();

		for (Date day : sortedArticles.keySet()) {
			cal.setTime(day);
			Element yearElement = getChildElementWithGivenAttribute(rootElem,
					"year", "value", String.valueOf(cal.get(cal.YEAR)));
			if (yearElement == null) {
				yearElement = createNewChildElementWithGivenAttribute(rootElem,
						"year", "value", String.valueOf(cal.get(cal.YEAR)));
			}
			Element monthElement = getChildElementWithGivenAttribute(
					yearElement, "month", "value",
					String.valueOf(cal.get(cal.MONTH) + 1));
			if (monthElement == null) {
				monthElement = createNewChildElementWithGivenAttribute(
						yearElement, "month", "value",
						String.valueOf(cal.get(cal.MONTH) + 1));
			}
			Element dayElement = getChildElementWithGivenAttribute(
					monthElement, "day", "value",
					String.valueOf(cal.get(cal.DAY_OF_MONTH)));
			if (dayElement == null) {
				dayElement = createNewChildElementWithGivenAttribute(
						monthElement, "day", "value",
						String.valueOf(cal.get(cal.DAY_OF_MONTH)));
			} else {
				// if day exists, remove and recreate
				monthElement.removeChild(dayElement);
				dayElement = createNewChildElementWithGivenAttribute(
						monthElement, "day", "value",
						String.valueOf(cal.get(cal.DAY_OF_MONTH)));
			}

			Map<String, Set<ArticleDTO>> regionBasedArticles = new HashMap<String, Set<ArticleDTO>>();

			for (ArticleDTO article : sortedArticles.get(day)) {
				if (regionBasedArticles.containsKey(article.getRegion()
						.toString())) {
					regionBasedArticles.get(article.getRegion().toString())
							.add(article);
				} else {
					regionBasedArticles.put(article.getRegion().toString(),
							new HashSet<ArticleDTO>());
					regionBasedArticles.get(article.getRegion().toString())
							.add(article);
				}
			}
			for (String region : regionBasedArticles.keySet()) {
				// checkRegionExists and create if needed
				Element regionElem = getChildElementWithGivenAttribute(
						dayElement, "region", "name", region);
				if (regionElem == null) {
					regionElem = createNewChildElementWithGivenAttribute(
							dayElement, "region", "name", region);
				}
				for (ArticleDTO article : regionBasedArticles.get(region)) {

					Element art = doc.createElement("article");
					Element articleName = doc.createElement("name");
					articleName.setTextContent(article.getName());
					Element articleLink = doc.createElement("link");
					articleLink.setTextContent(article.getReferenceURL());
					Element articleCity = doc.createElement("city");
					articleCity.setTextContent(article.getCity());
					art.appendChild(articleName);
					art.appendChild(articleLink);
					art.appendChild(articleCity);
					regionElem.appendChild(art);
				}
			}

		}
		try {
			serializetoXML(USER_APP_HOME + "DataStorage.xml");
			Properties properties = new Properties();
			InputStream is = new FileInputStream(USER_APP_HOME
					+ "usage.properties");
			properties.load(is);

			cal.setTime(new Date());
			properties.setProperty("lastUpdateYear",
					String.valueOf(cal.get(cal.YEAR)));
			properties.setProperty("lastUpdateMonth",
					String.valueOf(cal.get(cal.MONTH)));
			properties.setProperty("lastUpdateDay",
					String.valueOf(cal.get(cal.DAY_OF_MONTH)));

			properties.store(new FileOutputStream(USER_APP_HOME
					+ "usage.properties"), "");
		} catch (TransformerConfigurationException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (TransformerException e) {
			e.printStackTrace();
		}
	}

	/**
	 * returns child elem whether child elem with given attribute exists or null
	 * 
	 * 
	 * @param currentElement
	 * @param childElementName
	 * @param attributeName
	 * @param attributeValue
	 * @return
	 */
	private static Element getChildElementWithGivenAttribute(
			Element parentElement, String childElementName,
			String attributeName, String attributeValue) {
		NodeList childElems = parentElement
				.getElementsByTagName(childElementName);
		Element child = null;
		for (int i = 0; i < childElems.getLength(); i++) {
			Element childElem = (Element) childElems.item(i);
			if (childElem.getAttribute(attributeName).equalsIgnoreCase(
					attributeValue)) {
				child = childElem;
				break;
			}
		}
		return child;
	}

	private static Element createNewChildElementWithGivenAttribute(
			Element parentElement, String elementName, String attributeName,
			String attributeValue) {
		Element newElement = doc.createElement(elementName);
		newElement.setAttribute(attributeName, attributeValue);
		parentElement.appendChild(newElement);
		return newElement;
	}

	/**
	 * Gets the date of last update (from property file or XML storage)
	 * 
	 * @return
	 */
	public static Date getLastUpdateDate() {
		Properties properties = new Properties();
		try {
			InputStream is = new FileInputStream(USER_APP_HOME
					+ "usage.properties");
			properties.load(is);
		} catch (IOException e) {
		}
		Calendar cal = Calendar.getInstance();
		Date date = cal.getTime();
		if (Integer.parseInt(properties.getProperty("lastAvailableYear")) == 0
				&& Integer.parseInt(properties.getProperty("lastAvailableDay")) == 0
				&& Integer.parseInt(properties
						.getProperty("lastAvailableMonth")) == 0) {
			cal.add(cal.MONTH, -1);
			date = cal.getTime();
			setLastAvailableDate(cal);
			return cal.getTime();
		} else {

			cal.set(cal.YEAR,
					Integer.valueOf(properties.getProperty("lastUpdateYear")));
			cal.set(cal.MONTH,
					Integer.valueOf(properties.getProperty("lastUpdateMonth")));
			cal.set(cal.DATE,
					Integer.valueOf(properties.getProperty("lastUpdateDay")));
			return cal.getTime();
		}
	}

	/**
	 * In first start when last available date is not set, is this call to set
	 * date (last update last available) to one month before current date
	 * 
	 * @param cal
	 */
	private static void setLastAvailableDate(Calendar cal) {
		try {
			Properties properties = new Properties();

			InputStream is = new FileInputStream(USER_APP_HOME
					+ "usage.properties");
			properties.load(is);

			// cal.setTime(new Date());
			properties.setProperty("lastAvailableYear",
					String.valueOf(cal.get(cal.YEAR)));
			properties.setProperty("lastAvailableMonth",
					String.valueOf(cal.get(cal.MONTH)));
			properties.setProperty("lastAvailableDay",
					String.valueOf(cal.get(cal.DAY_OF_MONTH)));

			properties.store(new FileOutputStream(USER_APP_HOME
					+ "usage.properties"), "");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * return last available date where user can find news it needs to be called
	 * after getLastUpdate
	 * 
	 * @return
	 */

	public static Date getLastAvailableDate() {
		Properties properties = new Properties();
		try {
			InputStream is = new FileInputStream(USER_APP_HOME
					+ "usage.properties");
			properties.load(is);
		} catch (IOException e) {
		}
		Calendar cal = Calendar.getInstance();
		cal.set(cal.YEAR,
				Integer.valueOf(properties.getProperty("lastAvailableDay")));
		cal.set(cal.MONTH,
				Integer.valueOf(properties.getProperty("lastAvailableMonth")));
		cal.set(cal.DATE,
				Integer.valueOf(properties.getProperty("lastAvailableDay")));
		return cal.getTime();
	}

	public List<ArticleDTO> loadArticleListPerDay(Date day) {
		List<ArticleDTO> returnList = getArticleDataPerRequestedDay(day);
		return returnList;
	}

	public List<ArticleDTO> loadArticleListPerDayAndRegion(Date day,
			Region region) {

		List<ArticleDTO> requestedDayArticles = getArticleDataPerRequestedDay(day);

		if (region.equals(Region.CR)) {
			return requestedDayArticles;
		}

		List<ArticleDTO> returnList = new ArrayList<ArticleDTO>();
		for (ArticleDTO article : requestedDayArticles) {
			if (article.getRegion().equals(region)) {
				returnList.add(article);
			}
		}
		return returnList;
	}

	public List<ArticleDTO> loadArticleListFromDateToDateAndRegion(Date from,
			Date to, Region region) {

		List<ArticleDTO> dataForTimeFrameToProcess = new ArrayList<ArticleDTO>();
		Calendar fromCal = Calendar.getInstance();
		fromCal.setTime(from);

		Calendar toCal = Calendar.getInstance();
		toCal.setTime(to);

		//
		// if (fromCal.get(Calendar.YEAR) > toCal.get(Calendar.YEAR)
		// || fromCal.get(Calendar.MONTH) > toCal.get(Calendar.MONTH)
		// || fromCal.get(Calendar.DAY_OF_MONTH) > toCal
		// .get(Calendar.DAY_OF_MONTH)) {
		// throw new IllegalArgumentException("From date is after to date.");
		// }

		while (fromCal.get(Calendar.YEAR) != toCal.get(Calendar.YEAR)
				|| fromCal.get(Calendar.MONTH) != toCal.get(Calendar.MONTH)
				|| fromCal.get(Calendar.DAY_OF_MONTH) != toCal
						.get(Calendar.DAY_OF_MONTH)) {

			
				dataForTimeFrameToProcess
						.addAll(getArticleDataPerRequestedDay(fromCal.getTime()));
			
			fromCal.add(Calendar.DAY_OF_MONTH, 1);

		}
		// case when from and to equals
		dataForTimeFrameToProcess.addAll(getArticleDataPerRequestedDay(fromCal
				.getTime()));

		if (region.equals(Region.CR)) {
			return dataForTimeFrameToProcess;
		}

		List<ArticleDTO> returnList = new ArrayList<ArticleDTO>();
		for (ArticleDTO article : dataForTimeFrameToProcess) {
			if (article.getRegion().equals(region)) {
				returnList.add(article);
			}
		}

		return returnList;
	}

	private static void serializetoXML(String output) throws IOException,
			TransformerConfigurationException, TransformerException {
		TransformerFactory factory = TransformerFactory.newInstance();
		Transformer transformer = factory.newTransformer();
		DOMSource source = new DOMSource(doc);
		StreamResult result = new StreamResult(output);

		transformer.transform(source, result);
	}

	private static void loadDataFromStorage() {

		// FIXME teoreticky by sla vetsi efektivita overovanim zda doc je ci
		// neni
		// prazdny, tj zda je potreba ho parsovat ze souboru

		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		DocumentBuilder builder;
		try {

			builder = factory.newDocumentBuilder();
			File storage = new File(USER_APP_HOME + "DataStorage.xml");

			doc = builder.parse(storage.toURI().toString());

		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private List<ArticleDTO> getArticleDataPerRequestedDay(Date day) {
		loadDataFromStorage();

		List<ArticleDTO> articleList = new ArrayList<ArticleDTO>();

		Calendar cal = Calendar.getInstance();

		Element rootElem = (Element) doc.getFirstChild();

		cal.setTime(day);
		Element yearElement = getChildElementWithGivenAttribute(rootElem,
				"year", "value", String.valueOf(cal.get(cal.YEAR)));
		if (yearElement == null) {
			throw new IllegalArgumentException(
					"No data are available for requested year.");
		}
		Element monthElement = getChildElementWithGivenAttribute(yearElement,
				"month", "value", String.valueOf(cal.get(cal.MONTH) + 1));
		if (monthElement == null) {
			throw new IllegalArgumentException(
					"No data are available for requested month.");
		}
		Element dayElement = getChildElementWithGivenAttribute(monthElement,
				"day", "value", String.valueOf(cal.get(cal.DAY_OF_MONTH)));
		if (dayElement == null) {
			System.out.println(String.format(
					"No data are available for requested day: %s", cal
							.getTime().toString()));
			return new ArrayList<>();
		}

		NodeList regions = dayElement.getElementsByTagName("region");

		for (int i = 0; i < regions.getLength(); i++) {
			Element regionElem = (Element) regions.item(i);
			Region r = Region.valueOf(regionElem.getAttribute("name"));
			NodeList articles = regionElem.getElementsByTagName("article");

			for (int j = 0; j < articles.getLength(); j++) {
				ArticleDTO art = new ArticleDTO();
				art.setRegion(r);
				art.setDate(day);
				Element articleElem = (Element) articles.item(j);
				NodeList articleDetails = articleElem.getChildNodes();

				for (int k = 0; k < articleDetails.getLength(); k++) {
					Node articleDetail = articleDetails.item(k);
					if (articleDetail.getNodeName().equals("name")) {
						art.setName(articleDetail.getTextContent());
						continue;
					}
					if (articleDetail.getNodeName().equals("link")) {
						art.setReferenceURL(articleDetail.getTextContent());
						continue;
					}
					if (articleDetail.getNodeName().equals("city")) {
						art.setCity(articleDetail.getTextContent());
					}
				}
				articleList.add(art);
			}
		}
		return articleList;
	}

	public static void checkAndCreateAppDirStructure() {
		File appDir = new File(USER_HOME + "localNews");
		if (!appDir.exists()) {
			try {
				appDir.mkdir();
				File prop = new File(USER_APP_HOME + "usage.properties");
				File storage = new File(USER_APP_HOME + "DataStorage.xml");

				InputStream isPropTemplate = XmlStorageHelper.class
						.getResourceAsStream("/usage.properties.template");
				InputStreamReader ir = new InputStreamReader(isPropTemplate);
				BufferedReader br = new BufferedReader(ir);

				prop.createNewFile();

				BufferedWriter bw = new BufferedWriter(new FileWriter(prop));

				while (br.ready()) {
					bw.write(br.readLine()
							+ System.getProperty("line.separator"));
				}
				bw.close();
				br.close();

				InputStream isDataStorageTemplate = XmlStorageHelper.class
						.getResourceAsStream("/DataStorageTemplate.xml");
				ir = new InputStreamReader(isDataStorageTemplate);
				br = new BufferedReader(ir);

				storage.createNewFile();

				bw = new BufferedWriter(new FileWriter(storage));

				while (br.ready()) {
					bw.write(br.readLine()
							+ System.getProperty("line.separator"));
				}
				bw.close();
				br.close();

			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}
